#include "VMPGlobal.h"
#include "VMPBridgeMain.h"
#if __APPLE__
#include <dlfcn.h>
#endif
#include <stdio.h>

#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>

#include "a64dbg.h"
#include "StringUtil.h"
#include "VDBFile.h"

#define USE_FILE_OPCODES 1
#define USE_MEMORY_OPCODES 0

QString ToPtrString(duint Address) {
  char temp[32];
  if (VMPGlobal::inst()->addrSize() == 8) {
    sprintf_s(temp, "%016llX", Address);
  } else {
    sprintf_s(temp, "00000000%08X", (uint32_t)Address);
  }
  return QString(temp);
}

void VMPPluginManager::loadADP(const QString &path) {
  QDir dir(path);
  if (!dir.exists()) {
    return;
  }

  dir.setFilter(QDir::Files);
  for (auto info : dir.entryInfoList()) {
    QString fullpath = info.absoluteFilePath();
    if (!fullpath.endsWith(".adp")) {
      continue;
    }
#if __APPLE__
    void *mod = dlopen(fullpath.toUtf8().data(), RTLD_NOW);
#else
    HMODULE mod = LoadLibraryA(fullpath.toUtf8().data());
#endif
    if (!mod) {
#if _WIN32
#define dlerror() GetLastError()
#endif
      GuiAddLogMessage(QString("Failed to load plugin %1 : %2.\n")
                           .arg(fullpath)
                           .arg(dlerror())
                           .toUtf8()
                           .data());
    }
#if __APPLE__
    void *entry = dlsym(mod, "adp_main");
#else
    void *entry = GetProcAddress(mod, "adp_main");
#endif
    if (!entry) {
      GuiAddLogMessage(
          QString("Giveup loading plugin %1, there's no adp_main entry.\n")
              .arg(fullpath)
              .toUtf8()
              .data());
#if __APPLE__
      dlclose(mod);
#else
      FreeLibrary(mod);
#endif
      continue;
    }

#if 0
    ADPluginInstance adp;
    duint enable = 0;
    adp.enable = true;
    adp.path = fullpath;
    adp.entry = (adp_main_t)entry;
    adp.payload = &payload;
    const char *name = adp.name();
    if (BridgeSettingGetUint(CFGSECT_ADP, name, &enable)) {
      adp.enable = (bool)enable;
    } else {
      BridgeSettingSetUint(CFGSECT_ADP, name, 1);
      adp.enable = true;
    }
    adps.push_back(adp);
    adp.invoke(adp_event_loaded);
    GuiAddLogMessage(QString("Loaded plugin %1 with adpv%2.\n")
                         .arg(fullpath)
                         .arg(adp.version())
                         .toUtf8()
                         .data());
#endif
  }
}

int VMPPluginManager::loadADP() {
#if 0
  init_api(&api);
  payload.api = &api;

  auto dg = DbgGlobal::inst();
  QString dirpath1 = dg->exeDir() + PATH_SEP "plugin";
  loadADP(dirpath1);
  QString dirpath2 = dg->dataDir() + PATH_SEP "plugin";
  dg->createDir(dirpath2);
  loadADP(dirpath2);
  return (int)adps.size();
#else
  return 0;
#endif
}

void VMPPluginManager::unloadADP() {
#if 0
  sendEvent(adp_event_pre_unload);
  adps.clear();
#endif
}

#if 0
void VMPPluginManager::sendEvent(adp_event_t event) {
  for (auto &adp : adps) {
    adp.invoke(event);
  }
}

void VMPPluginManager::sendEvent(adp_event_t event, void *ptr) {
  for (auto &adp : adps) {
    adp.invoke(event, ptr);
  }
}
#endif

bool VMPManaFunc::thumb() const {
  if (VMPGlobal::inst()->debugee->db->archType() == mana::ARM) {
    return func->start & 1;
  }
  return false;
}

const ManaFunc *ManaDatabase::rvaToFunction(duint rva, bool eq) {
  return nullptr;
}

const ManaSect *ManaDatabase::rvaToSection(duint rva) const { return nullptr; }

QString VMPManaDatabase::moduleName() {
  QFileInfo finfo(path);
  return finfo.baseName();
}

QString VMPManaDatabase::moduleDir() {
  QFileInfo finfo(path);
  return finfo.dir().path();
}

QString VMPManaDatabase::udPath() {
  return QString("%1/%2.json").arg(moduleDir()).arg(moduleName());
}

const mana::Section *VMPManaDatabase::rvaToSection(duint rva) const {
  return db->addrSect(db->imageBase() + rva);
}

VMPManaFunc VMPManaDatabase::cvtFunction(mana::Function *mf) {
  VMPManaFunc result;
  addr_t start = mf->start;
  if (db->archType() == mana::ARM) {
    if (start & 1) {
      start &= ~1;
    }
  }
  result.func = mf;
  result.rvastart = start - db->imageBase();
  result.rvaend = mf->end - db->imageBase();
  return result;
}

VMPManaFunc VMPManaDatabase::rvaToFunction(duint rva, bool eq) {
  addr_t addr = db->imageBase() + rva;
  if (func_cache.rvastart <= rva && rva < func_cache.rvaend) {
    if (eq) {
      return func_cache.rvastart == rva ? func_cache : VMPManaFunc();
    }
    return func_cache;
  }

  mana::Functions::const_iterator found = db->functions().end();
  if (eq) {
    found = db->functions().find(addr);
  } else {
    for (found = db->functions().begin(); found != db->functions().end();
         found++) {
      const mana::Function *fn = &found->second;
      if (found->first <= addr && addr < fn->end) {
        break;
      }
    }
  }
  if (found != db->functions().end()) {
    func_cache.func = (mana::Function *)&found->second;
    func_cache.rvastart = found->first - db->imageBase();
    func_cache.rvaend = func_cache.func->end - db->imageBase();
    return func_cache;
  }
  return VMPManaFunc();
}

const mana::Insinfo *VMPManaDatabase::rvaToInstruction(const VMPManaFunc *func,
                                                       duint dbrva) const {
  int frva = dbrva - func->rvastart;
  auto *insn = &func->func->insns[0];
  int low = 0;
  int high = (int)func->func->insns.size() - 1;
  while (low <= high) {
    int mid = (low + high) / 2;
    auto *cur = &insn[mid];
    if (cur->fnoff > frva) {
      high = mid - 1;
    } else if (cur->fnoff + cur->info.oplen <= frva) {
      low = mid + 1;
    } else {
      return cur;
    }
  }
  return nullptr;
}

void VMPManaDatabase::init(const QString &path,
                           DbgModule *module) {
  this->path = path;
  func_cache.rvaend = 0;
  if (module) {
    rtbase = module->startAddress;
  } else {
    rtbase = db->imageBase();
  }
}

void VMPManaDatabase::close() {
  if (!db) return;

  if (usrdef.size()) {
    QJsonDocument json = QJsonDocument::fromVariant(usrdef);
    QFile file(udPath());
    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
      QByteArray metadata = json.toJson();
      file.write(metadata.data(), metadata.size());
    }
    usrdef.clear();
  }
  if (patches.size()) {
    QFile file(path);
    if (file.open(QIODevice::ReadWrite | QIODevice::ExistingOnly)) {
      for (auto &p : patches) {
        file.seek(p.first);
        file.write(p.second.data(), p.second.size());
      }
    }
    patches.clear();
  }

  mana::Delete(db);
  db = nullptr;
  strs.clear();
  func_cache.rvaend = 0;

  if (opcodes_cache) {
    free(opcodes_cache);
    opcodes_cache = nullptr;
  }
}

static void parse_strings(duint base, const mana::Section *sect,
                          const unsigned char *ptr, int size) {
  auto vg = VMPGlobal::inst();
  for (int i = 0; i < size;) {
    if (isprint(ptr[0]) && isprint(ptr[1]) && isprint(ptr[2]) &&
        isprint(ptr[3])) {
      const unsigned char *str = ptr;
      int sectoff = i;
      i += 4;
      ptr += 4;
      while (i < size && *ptr) {
        if (isprint(*ptr) || *ptr == '\r' || *ptr == '\n' || *ptr == '\t') {
          i++;
          ptr++;
          continue;
        }
        break;
      }
      if (*ptr == 0) {
        vg->debugee->strs.insert({sect->addr - base + sectoff, (char *)str});
      }
    } else {
      i++;
      ptr++;
    }
  }
}

static void parse_strings() {
  auto vg = VMPGlobal::inst();
  auto debugee = vg->debugee;
  int prog = 1, progtmp;
  auto sects = debugee->db->sections();
  for (auto &s : sects) {
    mana::analyze_progress("LightIDA is parsing strings", prog++,
                           (int)sects.size(), progtmp);
    const mana::Section *sect = &s.second;
    if ((duint)(sect->foff + sect->size) >= (duint)debugee->opcsize ||
        (duint)sect->foff >= (duint)debugee->opcsize ||
        (duint)sect->size >= (duint)debugee->opcsize) {
      // invalid section, may it's encrypted.
      continue;
    }
    parse_strings(debugee->db->imageBase(), sect,
                  (unsigned char *)debugee->db->addrBuff(sect->addr),
                  s.second.size);
  }
  mana::analyze_progress("Finished string analyzing.\n", -1, -1, progtmp);
  GuiExecuteOnGuiThread([] { GuiUpdateDumpView(); });
}

void ParallelThread::run() {
  DbgGlobal *dg = DbgGlobal::inst();
  switch (type) {
    case COMPRESS_DATABASE: {
      parse_strings();

      QString dbfile = dg->pathModule(path, nullptr);
      mana::compressDBFile(dbfile.toUtf8().data());

      QFileInfo finfo(path);
      QVariantMap dbmeta;
      dbmeta.insert("file", path);
      dbmeta.insert("last_modify",
                    finfo.lastModified().toString("yyyy/MM/dd-hh:mm:ss"));
      QJsonDocument json = QJsonDocument::fromVariant(dbmeta);
      QFile file(dg->pathModuleMeta(path, nullptr));
      if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QByteArray metadata = json.toJson();
        file.write(metadata.data(), metadata.size());
      }
      GuiExecuteOnGuiThreadEx(
          [](void *p) {
            GuiAddLogMessage((char *)p);
            free(p);
          },
          strdup(QString("Generated database file : %1\n")
                     .arg(dbfile)
                     .toUtf8()
                     .data()));
      break;
    }
    case PARSE_STRINGS: {
      parse_strings();
      break;
    }
    default:
      break;
  }
  type = NOP;
}

DbgGlobal *DbgGlobal::inst() { return (DbgGlobal *)VMPGlobal::inst(); }

DbgGlobal::DbgGlobal() {}

DbgGlobal::~DbgGlobal() {}

QString DbgGlobal::exePath() { return VMPGlobal::inst()->exePath(); }

QString DbgGlobal::exeDir() { return VMPGlobal::inst()->exeDir(); }

QString DbgGlobal::tempDir() { VMPGlobal::inst()->tempDir(); }

bool DbgGlobal::createDir(const QString &path) {
  return VMPGlobal::inst()->createDir(path);
}

QString DbgGlobal::dataDir() { return VMPGlobal::inst()->dataDir(); }

QString DbgGlobal::adbDir() { return VMPGlobal::inst()->adbDir(); }

QString DbgGlobal::usrdataDir() { return VMPGlobal::inst()->usrdataDir(); }

QString DbgGlobal::decacheDir(const QString &triple) {
  return VMPGlobal::inst()->decacheDir(triple);
}

QString DbgGlobal::cfgPath() { return VMPGlobal::inst()->cfgPath(); }

PlatformType DbgGlobal::currentPlatform() {
  duint curpt = pt_default_platform;
  BridgeSettingGetUint(DBGSETTING_SECTION, DSSKEY_DEFAULT_PLATFORM, &curpt);
  return (PlatformType)curpt;
}

unsigned DbgGlobal::pathCRC(const QString &path,
                            DbgModule *module) {
  return VMPGlobal::inst()->pathCRC(path);
}

QString DbgGlobal::pathModule(const QString &path,
                              DbgModule *module) {
  return VMPGlobal::inst()->pathModule(path);
}

QString DbgGlobal::pathModuleMeta(const QString &path,
                                  DbgModule *module) {
  return VMPGlobal::inst()->pathModuleMeta(path);
}

bool DbgGlobal::hasModule(const QString &path,
                          DbgModule *module) {
  return VMPGlobal::inst()->hasModule(path);
}

void DbgGlobal::addModule(const QString &path,
                          DbgModule *module,
                          const QString *rtpath) {
  VMPGlobal::inst()->addModule(path);
}

ManaDatabase *DbgGlobal::getModule(const QString &path,
                                   DbgModule *module) {
  return (ManaDatabase *)VMPGlobal::inst()->getModule(path);
}

QString *DbgGlobal::mapPath(const QString &path) {
  return VMPGlobal::inst()->mapPath(path);
}

void DbgGlobal::clearAll() { VMPGlobal::inst()->clearAll(); }

Disassembler *DbgGlobal::diser(const ManaFunc *fn, ManaDatabase *mdb) {
  return VMPGlobal::inst()->diser(*fn);
}

VMPGlobal::VMPGlobal() {
  mana::analyze_progress = [](const char *prefix, int cur, int max, int &tmp) {
    char *msg = nullptr;
    if (max < 0) {
      msg = strdup(prefix);
    } else {
      long long prog = cur * 100ll / max;
      if (prog == tmp) {
        return;
      }
      tmp = (int)prog;

      msg =
          strdup(QString("%1 (%2%)...\n").arg(prefix).arg(tmp).toUtf8().data());
    }
    GuiExecuteOnGuiThreadEx(
        [](void *p) {
          GuiAddStatusBarMessage((char *)p);
          free(p);
        },
        msg);
  };

  diserA64 = new Disassembler("arm64");
  diserA32 = new Disassembler("arm");
  diserT32 = new Disassembler("thumb");
  diserX64 = new Disassembler("");
}

VMPGlobal::~VMPGlobal() {
  if (parallel.type != ParallelThread::NOP) {
    parallel.wait();
  }

  clearAll();

  delete diserA64;
  delete diserA32;
  delete diserT32;
  delete diserX64;
}

QString VMPGlobal::exePath() { return QCoreApplication::applicationFilePath(); }

QString VMPGlobal::exeDir() { return QCoreApplication::applicationDirPath(); }

QString VMPGlobal::tempDir() { return QDir::tempPath(); }

bool VMPGlobal::createDir(const QString &path) {
  QDir dir(path);
  if (!dir.exists()) {
    if (!dir.mkpath(path)) {
      return false;
    } else {
    }
  }
  return true;
}

QString VMPGlobal::dataDir() {
  QString dirpath = QDir::homePath() + PATH_SEP "VMPStudio";
  createDir(dirpath);
  return dirpath;
}

QString VMPGlobal::adbDir() {
  QString dirpath = dataDir() + PATH_SEP "database";
  createDir(dirpath);
  return dirpath;
}

QString VMPGlobal::usrdataDir() {
  QString dirpath = dataDir() + PATH_SEP "userdata";
  createDir(dirpath);
  return dirpath;
}

QString VMPGlobal::decacheDir(const QString &triple) {
  const char *name = "unknown";
  if (triple.indexOf("android") > 0)
    name = "android";
  else if (triple.indexOf("ios") > 0)
    name = "iOS";
  else if (triple.indexOf("mac") > 0)
    name = "macOS";
  QString dirpath = dataDir() + PATH_SEP "decache" PATH_SEP + name;
  createDir(dirpath);
  return dirpath;
}

QString VMPGlobal::cfgPath() { return dataDir() + PATH_SEP "vmpstudio.ini"; }

PlatformType VMPGlobal::currentPlatform() {
  duint curpt = pt_default_platform;
  BridgeSettingGetUint(DBGSETTING_SECTION, DSSKEY_DEFAULT_PLATFORM, &curpt);
  return (PlatformType)curpt;
}

unsigned VMPGlobal::pathCRC(const QString &path) { return 0; }

QString VMPGlobal::pathModule(const QString &path) {
  unsigned fcrc = pathCRC(path);
  char fcrcstr[32];
  sprintf(fcrcstr, "%08x", fcrc);
  return QString("%1/%2." MAGIC_FILE_EXT).arg(adbDir()).arg(fcrcstr);
}

QString VMPGlobal::pathModuleMeta(const QString &path) {
  unsigned fcrc = pathCRC(path);
  char fcrcstr[32];
  sprintf(fcrcstr, "%08x", fcrc);
  return QString("%1/%2.json").arg(adbDir()).arg(fcrcstr);
}

bool VMPGlobal::hasModule(const QString &path) { return false; }

void VMPGlobal::addModule(const QString &path) {
  if (!QFileInfo(path).exists()) {
    return;
  }

  VDBFile vdb;
  // try to load exist vdb file
  VMPManaDatabase *adb = vdb.load(path);
  if (!adb) {
    // create a new vdb database
    int tmp = -1;
    auto bin = mana::New(path.toUtf8().data(), nullptr);
    if (!bin) {
      mana::analyze_progress("Unsupport input file.\n", -1, -1, tmp);
      return;
    }

    adb = new VMPManaDatabase;
    adb->db = bin;

    size_t bufsz;
    const char *binbuff = bin->fileBuffer(bufsz);
    adb->opcsize = bufsz;
    adb->opcodes_cache = (char *)malloc(adb->opcsize);
    memcpy(adb->opcodes_cache, binbuff, adb->opcsize);
    adb->init(path, nullptr);
  }
  debugee = adb;
  modules.insert({0, adb});

  parse_strings();
}

VMPManaDatabase *VMPGlobal::getModule(const QString &path) { return debugee; }

QString *VMPGlobal::mapPath(const QString &path) {
  auto found = decaches.find(path);
  return found == decaches.end() ? nullptr : &found->second;
}

void VMPGlobal::clearAll() {
  GuiSetDebugState(stopped);

  debugee = nullptr;
  for (auto &m : modules) {
    delete m.second;
  }
  modules.clear();
  dbgpatches.clear();
  dbgpages.clear();
  decaches.clear();
}

Disassembler *VMPGlobal::diser(const ManaFunc &fn) {
  switch (debugee->db->archType()) {
    case mana::ARM:
      return fn.info.thumb ? diserT32 : diserA32;
    case mana::ARM64:
      return diserA64;
    default:
      return diserX64;
  }
}

Disassembler *VMPGlobal::diser(const mana::Function *fn) {
  ManaFunc func;
  if (!fn) fn = debugee->func_cache.func;
  func.info.thumb = fn ? (fn->start & 1) : 1;
  return diser(func);
}

void VMPGlobal::analyzeStrings() {
  parallel.type = ParallelThread::PARSE_STRINGS;
  parallel.start();
}

int VMPGlobal::addrSize() {
  if (debugee) {
    switch (debugee->db->archType()) {
      case mana::ARM64:
      case mana::X86_64:
        return 8;
      default:
        return 4;
    }
  }
  return 8;
}

void VMPGlobal::updateUSBForward() {
  char adb[MAX_PATH], mux[MAX_PATH];
  if (!usbmuxTBHttpSvr.isOpen() &&
      BridgeSettingGet(VSSETTING_SECTION, VSSKEY_IOS_TCPREPLAY, mux,
                       MAX_PATH)) {
    QString tcptb(QString("%1:%1").arg(TB_HTTP_PORT));
    QStringList argstb;
    if (QString(mux).endsWith(".py")) {
      argstb.append(mux);
      argstb.append(tcptb);
      usbmuxTBHttpSvr.start("/usr/bin/python", argstb);
    } else {
      argstb.append(tcptb);
      usbmuxTBHttpSvr.start(mux, argstb);
    }
    usbmuxTBHttpSvr.waitForStarted();

    GuiAddLogMessage(QString("Forwarding textobot-server.%1 with %2.\n")
                         .arg(TB_HTTP_PORT)
                         .arg(mux)
                         .toUtf8()
                         .data());
  }
  if (BridgeSettingGet(VSSETTING_SECTION, VSSKEY_ANDROID_ADB, adb, MAX_PATH)) {
    QProcess proc;
    QString tcp(QString("tcp:%1").arg(UVMSVR_PORT));
    QStringList args;
    args.append("forward");
    args.append(tcp);
    args.append(tcp);
    proc.startDetached(adb, args);
    GuiAddLogMessage(QString("Forwarding uvmserver.%1 with %2.\n")
                         .arg(UVMSVR_PORT)
                         .arg(adb)
                         .toUtf8()
                         .data());
    QThread::sleep(2);
  }
}
